﻿#region copyright

  //This file is part of the Twitterizer library (http://www.twitterizer.net/)
 
  //Copyright (c) 2010/2011, Patrick "Ricky" Smith (ricky@digitally-born.com)
  //All rights reserved.

#endregion

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.ServiceModel.Web;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web;

namespace WFExtended.Twitter
{
    /// <summary>
    /// OAuth Header Authorization Builder
    /// </summary>
    public class OAuthAuthorization
    {
        public OAuthAuthorization(OAuthTokens tokens, Uri uri, string verb)
        {
            _tokens = tokens;
            _uri = uri;
            _verb = verb;

            if (uri.ToString().Contains('?'))
            {
                System.Collections.Specialized.NameValueCollection parameters =
                    System.Web.HttpUtility.ParseQueryString(uri.ToString().Substring(uri.ToString().IndexOf('?')));

                for (int i = 0; i < parameters.Count; i++)
                {
                    _parameters.Add(parameters.Keys[i], HttpUtility.UrlDecode(parameters[i]));
                }
            }

            if(!String.IsNullOrEmpty(_uri.Query))
                _uri = new Uri(_uri.AbsoluteUri.Replace(_uri.Query, String.Empty));
        
        }

        private static readonly string[] SecretParameters = new[]
                                                                {
                                                                    "oauth_consumer_secret",
                                                                    "oauth_token_secret",
                                                                    "oauth_signature"
                                                                };

        private static readonly string[] OAuthParametersToIncludeInHeader = new[]
                                                          {
                                                              "oauth_version",
                                                              "oauth_nonce",
                                                              "oauth_timestamp",
                                                              "oauth_signature_method",
                                                              "oauth_consumer_key",
                                                              "oauth_token",
                                                              "oauth_verifier"
                                                          };

        private OAuthTokens _tokens = null;

        private Dictionary<string, object> _parameters = new Dictionary<string, object>();

        private Uri _uri;

        private string _verb;

        private const string Realm = "Twitter API";

        /// <summary>
        /// Generates the OAuth authorization header.
        /// </summary>
        /// <returns></returns>
        public string GenerateAuthorizationHeader()
        {
            SetupOAuth();
            AddQueryStringParametersToUri();

            StringBuilder authHeaderBuilder = new StringBuilder();
            authHeaderBuilder.AppendFormat("OAuth realm=\"{0}\"", Realm);

            var sortedParameters = from p in this._parameters
                                   where OAuthParametersToIncludeInHeader.Contains(p.Key)
                                   orderby p.Key, UrlEncode((p.Value is string) ? (string)p.Value : string.Empty)
                                   select p;

            foreach (var item in sortedParameters)
            {
                authHeaderBuilder.AppendFormat(
                    ",{0}=\"{1}\"",
                    UrlEncode(item.Key),
                    UrlEncode(item.Value as string));
            }

            authHeaderBuilder.AppendFormat(",oauth_signature=\"{0}\"", UrlEncode(_parameters["oauth_signature"] as string));

            return authHeaderBuilder.ToString();
        }

        private void AddQueryStringParametersToUri()
        {
            StringBuilder requestParametersBuilder = new StringBuilder(_uri.AbsoluteUri);
            requestParametersBuilder.Append(_uri.Query.Length == 0 ? "?" : "&");


            Dictionary<string, object> fieldsToInclude = new Dictionary<string, object>(_parameters.Where(p => !OAuthParametersToIncludeInHeader.Contains(p.Key) &&
                                         !SecretParameters.Contains(p.Key)).ToDictionary(p => p.Key, p => p.Value));

            foreach (KeyValuePair<string, object> item in fieldsToInclude)
            {
                if (item.Value is string)
                    requestParametersBuilder.AppendFormat("{0}={1}&", item.Key, UrlEncode((string)item.Value));
            }

            if (requestParametersBuilder.Length == 0)
                return;

            requestParametersBuilder.Remove(requestParametersBuilder.Length - 1, 1);

            _uri = new Uri(requestParametersBuilder.ToString());
        }

        private byte[] GetMultipartFormData(Dictionary<string, object> param, string boundary)
        {
            Stream formDataStream = new MemoryStream();
            Encoding encoding = Encoding.UTF8;

            Dictionary<string, object> fieldsToInclude = new Dictionary<string, object>(param.Where(p => !OAuthParametersToIncludeInHeader.Contains(p.Key) &&
                             !SecretParameters.Contains(p.Key)).ToDictionary(p => p.Key, p => p.Value));

            foreach (KeyValuePair<string, object> kvp in fieldsToInclude)
            {
                if (kvp.Value.GetType() == typeof(Stream))
                {	//assume this to be a byte stream
                    Stream data = (Stream)kvp.Value;

                    string header = string.Format("--{0}\r\nContent-Disposition: form-data; name=\"{1}\"; filename=\"{2}\";\r\nContent-Type: application/octet-stream\r\n\r\n",
                        boundary,
                        kvp.Key,
                        kvp.Key);

                    byte[] headerBytes = encoding.GetBytes(header);
                    byte[] bytes = System.Text.Encoding.UTF8.GetBytes(new StreamReader(data).ReadToEnd());
                    formDataStream.Write(headerBytes, 0, headerBytes.Length);
                    formDataStream.Write(bytes, 0, bytes.Length);
                }
                else
                {	//this is normal text data
                    string header = string.Format("--{0}\r\nContent-Disposition: form-data; name=\"{1}\"\r\n\r\n{2}\r\n",
                        boundary,
                        kvp.Key,
                        kvp.Value);

                    byte[] headerBytes = encoding.GetBytes(header);

                    formDataStream.Write(headerBytes, 0, headerBytes.Length);
                }
            }

            string footer = string.Format("\r\n--{0}--\r\n", boundary);
            formDataStream.Write(encoding.GetBytes(footer), 0, footer.Length);
            formDataStream.Position = 0;
            byte[] returndata = new byte[formDataStream.Length];

            formDataStream.Read(returndata, 0, returndata.Length);
            formDataStream.Close();

            return returndata;
        }

        #region OAuth

        private void SetupOAuth()
        {

            // Add the OAuth parameters
            _parameters.Add("oauth_version", "1.0");
            _parameters.Add("oauth_nonce", GenerateNonce());
            _parameters.Add("oauth_timestamp", GenerateTimeStamp());
            _parameters.Add("oauth_signature_method", "HMAC-SHA1");
            _parameters.Add("oauth_consumer_key", _tokens.ConsumerKey);
            _parameters.Add("oauth_consumer_secret", this._tokens.ConsumerSecret);

            if (String.IsNullOrEmpty(_tokens.UserAccessToken))
            {
                _parameters.Add("oauth_token", this._tokens.ApplicationAccessToken);
                _parameters.Add("oauth_token_secret", this._tokens.ApplicationAccessTokenSecret);
            }
            else
            {
                _parameters.Add("oauth_token", this._tokens.UserAccessToken);
                _parameters.Add("oauth_token_secret", this._tokens.UserAccessTokenSecret);
            }



            _parameters.Add("oauth_signature", GenerateSignature());
        }

        private static string GenerateNonce()
        {
            return new Random()
                .Next(123400, int.MaxValue)
                .ToString("X", CultureInfo.InvariantCulture);
        }

        private static string GenerateTimeStamp()
        {
            TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
            return Convert.ToInt64(ts.TotalSeconds, CultureInfo.CurrentCulture).ToString(CultureInfo.CurrentCulture);
        }

        private string GenerateSignature()
        {
            IEnumerable<KeyValuePair<string, object>> nonSecretParameters;


            nonSecretParameters = (from p in this._parameters
                                   where (!SecretParameters.Contains(p.Key))
                                   select p);

            Uri urlForSigning = _uri;

            // Create the base string. This is the string that will be hashed for the signature.
            string signatureBaseString = string.Format(
                CultureInfo.InvariantCulture,
                "{0}&{1}&{2}",
                _verb.ToUpper(CultureInfo.InvariantCulture),
                UrlEncode(NormalizeUrl(urlForSigning)),
                UrlEncode(nonSecretParameters));

            // Create our hash key (you might say this is a password)
            string key = string.Format(
                CultureInfo.InvariantCulture,
                "{0}&{1}",
                UrlEncode(_tokens.ConsumerSecret),
                UrlEncode((String.IsNullOrEmpty(_tokens.UserAccessTokenSecret)) ? _tokens.ApplicationAccessTokenSecret : _tokens.UserAccessTokenSecret));


            // Generate the hash
            HMACSHA1 hmacsha1 = new HMACSHA1(Encoding.UTF8.GetBytes(key));
            byte[] signatureBytes = hmacsha1.ComputeHash(Encoding.UTF8.GetBytes(signatureBaseString));
            return Convert.ToBase64String(signatureBytes);
        }

        private static string NormalizeUrl(Uri url)
        {
            string normalizedUrl = string.Format(CultureInfo.InvariantCulture, "{0}://{1}", url.Scheme, url.Host);
            if (!((url.Scheme == "http" && url.Port == 80) || (url.Scheme == "https" && url.Port == 443)))
            {
                normalizedUrl += ":" + url.Port;
            }

            normalizedUrl += url.AbsolutePath;
            return normalizedUrl;
        }

        private static string UrlEncode(string value)
        {
            if (string.IsNullOrEmpty(value))
            {
                return string.Empty;
            }

            value = Uri.EscapeDataString(value);

            // UrlEncode escapes with lowercase characters (e.g. %2f) but oAuth needs %2F
            value = Regex.Replace(value, "(%[0-9a-f][0-9a-f])", c => c.Value.ToUpper());

            // these characters are not escaped by UrlEncode() but needed to be escaped
            value = value
                .Replace("(", "%28")
                .Replace(")", "%29")
                .Replace("$", "%24")
                .Replace("!", "%21")
                .Replace("*", "%2A")
                .Replace("'", "%27");

            // these characters are escaped by UrlEncode() but will fail if unescaped!
            value = value.Replace("%7E", "~");

            return value;
        }

        private static string UrlEncode(IEnumerable<KeyValuePair<string, object>> parameters)
        {
            StringBuilder parameterString = new StringBuilder();

            var paramsSorted = from p in parameters
                               orderby p.Key, p.Value
                               select p;

            foreach (var item in paramsSorted)
            {
                if (item.Value is string)
                {
                    if (parameterString.Length > 0)
                    {
                        parameterString.Append("&");
                    }

                    parameterString.Append(
                        string.Format(
                            CultureInfo.InvariantCulture,
                            "{0}={1}",
                            UrlEncode(item.Key),
                            UrlEncode((string)item.Value)));
                }
            }

            return UrlEncode(parameterString.ToString());
        }

        #endregion

    }
}
